"""
Application of mutability:
    tuple -> list
    namedtuple -> dict
    set -> mutableset

Application of indexing:
    list -> ordereddict
    set -> dict
    set -> KeyedSet (keys are in the values)

Thus a KeyedSet is like a dict where the keys are in the values.  The
same could be applied to namedtuple (KeyedTuple) and to list 
(KeyedSequence).  It seems like there would not be place for KeyedDict
since __setitem__would not make sense and everything else is covered by 
KeyedSet

"""

import random as random_module
import collections as col_module

###############################################################################
# Module Exceptions

class MissingValueError(KeyError): pass

###############################################################################
# Module Classes

#class LazyKeyed(Keyed):
#    "Weakref?  Caching?  deferred execution?  deferred lookup?"


class KeyedSet(col_module.Set):
    """
    The keyed set is a cross between a set and a dict.  It indexes like 
    a dict, but otherwise works like a set.  The dict-like indexing 
    works by using the value of the attribute stored in __key__ of each 
    set item.  So, it is like a dict with the keys stored in each dict 
    value.
    
    """
    __key__ = "id"
    def __init__(self, iterable=()):
        self._dict = {}
        for item in iterable:
            self._add(item)
    def _get_key(self, item):
        # is this the right way to do it?
        try:
            key = item[self.__key__]
        except (KeyError, TypeError):
            try:
                key = getattr(item, self.__key__)
            except AttributeError:
                raise ValueError("Could not find key %s in item %s." % (
                        self.__key__,
                        item,
                        ))
        return key
    def _add(self, item):
        if item in self:
            # (address comparing keys vs. comparing values)
            raise KeyError("Item %s already added." % (item,))
            #return
            #pass
        key = self._get_key(item)
        self._dict[key] = item
    def __iter__(self):
        return iter(self._dict.values())
    def __contains__(self, item):
        key = self._get_key(item)
        if key not in self._dict:
            return False
        if self._dict[key] is None:
            raise MissingValueError(item)
        #if item != self[key]:
        #    ???
        return True
    def __len__(self):
        return len(self._dict)
    def __getitem__(self, key):
        return self._dict[key]
    get = col_module.Mapping.get.im_func
    def keys(self):
        return tuple(self._dict.keys())
    def one(self):
        #it = iter(self)
        #try: value = next(it)
        #except StopIteration: raise KeyError("Empty")
        #return value
        if len(self) == 0:
            raise KeyError("Empty")
        return self[random_module.choice(self.keys())]
    # handle comparisons too? (address comparing keys vs. comparing values)


class MutableKeyedSet(KeyedSet, col_module.MutableSet):
    def add(self, item):
        self._add(item)
    def discard(self, item):
        key = self._get_key(item)
        del self[key]
    def __delitem__(self, key):
        del self._dict[key]
    def pop(self):
        item = self.one()
        self.remove(item)
        return item
    # __setitem__ would turn the KeyedSet into a dict...


class LazyKeyedSet(KeyedSet):
    def __init__(self, iterable=[]):
        self._dict = {}
        type = None
        for item in iterable:
            if hasattr(item, "__key__"):
                if type == "keys":
                    raise TypeError("Requires either an iterable of keys or an iterable of Keyed, not both.")
                key = getattr(item, "__key__")
                if key in self._dict and item != self._dict[key]:
                    raise KeyMismatchError("Item with same key but different value.  Key: %s" % key)
                self._dict[key] = item
                type = "items"
            elif isinstance(item, str):
                if type == "items":
                    raise TypeError("Requires either an iterable of keys or an iterable of Keyed, not both.")
                self._dict[item] = None
                type = "keys"
            else:
                raise TypeError("Requires an iterable of string keys.")

    def __getitem__(self, key):
        "returns lazy objects or dynamic objects or cached objects?"
        if self._dict[key] is None:
            raise MissingValueError()
        return self._dict[key]
                

